{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import feast\n",
    "\n",
    "actual_version = feast.__version__\n",
    "assert actual_version == os.environ.get(\"FEAST_VERSION\"), (\n",
    "    f\"❌ Feast version mismatch. Expected: {os.environ.get('FEAST_VERSION')}, Found: {actual_version}\"\n",
    ")\n",
    "print(f\"✅ Found Expected Feast version: {actual_version} in workbench\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "namespace = os.environ.get(\"NAMESPACE\")  # read namespace from env\n",
    "if not namespace:\n",
    "    raise ValueError(\"NAMESPACE environment variable is not set\")\n",
    "\n",
    "yaml_content = os.popen(\n",
    "    f\"oc get configmap feast-credit-scoring-client -n {namespace} \"\n",
    "    \"-o jsonpath='{.data.feature_store\\\\.yaml}' | sed 's/\\\\\\\\n/\\\\n/g'\"\n",
    ").read()\n",
    "\n",
    "# Save the configmap data into an environment variable (if needed)\n",
    "os.environ[\"CONFIGMAP_DATA\"] = yaml_content\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#!/usr/bin/env python3\n",
    "import os\n",
    "import json\n",
    "import yaml\n",
    "from pathlib import Path\n",
    "from typing import Dict, List, Any, Optional\n",
    "from feast import FeatureStore\n",
    "from feast.repo_config import RepoConfig\n",
    "\n",
    "def create_feature_store_yaml(config_content: str, config_name: str) -> str:\n",
    "    \"\"\"\n",
    "    Create a feature_store.yaml file from config content.\n",
    "    \n",
    "    Args:\n",
    "        config_content: YAML content as string\n",
    "        config_name: Name identifier for the config (used for filename)\n",
    "    \n",
    "    Returns:\n",
    "        Path to the created YAML file\n",
    "    \"\"\"\n",
    "    # Parse the YAML content to validate it\n",
    "    try:\n",
    "        config_dict = yaml.safe_load(config_content)\n",
    "    except yaml.YAMLError as e:\n",
    "        raise ValueError(f\"Failed to parse YAML content for {config_name}: {e}\")\n",
    "    \n",
    "    # Ensure required fields are present\n",
    "    required_fields = ['project', 'registry', 'provider']\n",
    "    for field in required_fields:\n",
    "        if field not in config_dict:\n",
    "            raise ValueError(f\"Failed to create config {config_name}: missing required field '{field}'\")\n",
    "    \n",
    "    # Create filename\n",
    "    filename = f\"feature_store_{config_name}.yaml\"\n",
    "    filepath = Path(filename)\n",
    "    \n",
    "    # Write the YAML file\n",
    "    with open(filepath, 'w') as f:\n",
    "        yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False)\n",
    "    \n",
    "    return str(filepath)\n",
    "\n",
    "\n",
    "def create_feature_store_object(yaml_file_path: str) -> FeatureStore:\n",
    "    \"\"\"\n",
    "    Create a FeatureStore object from a YAML file.\n",
    "    \n",
    "    Args:\n",
    "        yaml_file_path: Path to the feature_store.yaml file\n",
    "    \n",
    "    Returns:\n",
    "        FeatureStore object\n",
    "    \"\"\"\n",
    "    try:\n",
    "        # Create FeatureStore from the YAML file\n",
    "        fs = FeatureStore(fs_yaml_file=Path(yaml_file_path))\n",
    "        return fs\n",
    "    except Exception as e:\n",
    "        raise RuntimeError(f\"Failed to create FeatureStore object from {yaml_file_path}: {e}\")\n",
    "\n",
    "\n",
    "def process_client_configs(client_configs: Dict[str, str]) -> Dict[str, Dict[str, Any]]:\n",
    "    \"\"\"\n",
    "    Process multiple client config YAML contents and create feature stores.\n",
    "    \n",
    "    Args:\n",
    "        client_configs: Dictionary mapping config names to YAML content strings\n",
    "    \n",
    "    Returns:\n",
    "        Dictionary with results for each config\n",
    "    \"\"\"\n",
    "    results = {}\n",
    "    created_yamls = []\n",
    "    feature_stores = {}\n",
    "    \n",
    "    print(\"Creating feature store YAMLs and objects...\")\n",
    "    print(\"=\" * 50)\n",
    "    \n",
    "    for config_name, config_content in client_configs.items():\n",
    "        try:\n",
    "            print(f\"\\nProcessing config: {config_name}\")\n",
    "\n",
    "            # Create YAML file\n",
    "            yaml_path = create_feature_store_yaml(config_content, config_name)\n",
    "            created_yamls.append(yaml_path)\n",
    "            print(f\"✓ Created YAML file: {yaml_path}\")\n",
    "            \n",
    "            # Create FeatureStore object\n",
    "            fs = create_feature_store_object(yaml_path)\n",
    "            fs_var_name = f\"fs_{fs.project}\"\n",
    "            globals()[fs_var_name] = fs\n",
    "            feature_stores[config_name] = fs_var_name\n",
    "            print(f\"✓ Created FeatureStore object: {fs_var_name}\")\n",
    "\n",
    "            results[config_name] = {\n",
    "                'yaml_path': yaml_path,\n",
    "                'feature_store': fs_var_name,\n",
    "                'project_name': fs.project,\n",
    "                'success': True,\n",
    "                'error': None\n",
    "            }\n",
    "            \n",
    "        except Exception as e:\n",
    "            print(f\"✗ Failed to process config {config_name}: {e}\")\n",
    "            results[config_name] = {\n",
    "                'yaml_path': None,\n",
    "                'feature_store': None,\n",
    "                'project_name': None,\n",
    "                'success': False,\n",
    "                'error': str(e)\n",
    "            }\n",
    "    \n",
    "    return results\n",
    "\n",
    "\n",
    "def print_summary(results: Dict[str, Dict[str, Any]]) -> None:\n",
    "    \"\"\"\n",
    "    Print summary of all operations.\n",
    "    \n",
    "    Args:\n",
    "        results: Results dictionary from process_client_configs\n",
    "    \"\"\"\n",
    "    print(\"\\n\" + \"=\" * 50)\n",
    "    print(\"SUMMARY\")\n",
    "    print(\"=\" * 50)\n",
    "\n",
    "    successful_configs = [name for name, result in results.items() if result['success']]\n",
    "    failed_configs = [name for name, result in results.items() if not result['success']]\n",
    "    print(f\"\\n\\n✓✓Feature Store YAML files have been created in: {os.getcwd()}\")\n",
    "    print(f\"\\n✓ Successfully processed {len(successful_configs)} config(s):\")\n",
    "    for config_name in successful_configs:\n",
    "        result = results[config_name]\n",
    "        print(f\"  - {config_name}: {result['yaml_path']} (Project: {result['project_name']})\")\n",
    "\n",
    "    if failed_configs:\n",
    "        print(f\"\\n✗ Failed to process {len(failed_configs)} config(s):\")\n",
    "        for config_name in failed_configs:\n",
    "            result = results[config_name]\n",
    "            print(f\"  - {config_name}: {result['error']}\")\n",
    "\n",
    "    print(f\"\\n\\n✓✓ Feature Store Object(s) details:\")\n",
    "    for config_name in successful_configs:\n",
    "        result = results[config_name]\n",
    "        print(f\"> Object Name - {result['feature_store']} ; project name - {result['project_name']} ; yaml path - {result['yaml_path']}\")\n",
    "\n",
    "    print(\"\\n\")\n",
    "    print(\"=\" * 25, \"Usage:\", \"=\" * 25)\n",
    "    print(\"You can now use feature store object(s) to access the feature store resources and functions!\")\n",
    "    print(\"\\n// Note: Replace object_name with the actual object name from the list above.\")\n",
    "    print(\"object_name.list_features()\\nobject_name.get_historical_features()\")\n",
    "    print(\"=\" * 58)\n",
    "\n",
    "\n",
    "def main():\n",
    "    \"\"\"\n",
    "    Main function to demonstrate usage with example configs.\n",
    "    \"\"\"\n",
    "    yaml_content = os.getenv(\"CONFIGMAP_DATA\")\n",
    "\n",
    "    if not yaml_content:\n",
    "        raise ValueError(\"CONFIGMAP_DATA environment variable is not set.\")\n",
    "\n",
    "    # Use environment YAML as config\n",
    "    client_configs = {\n",
    "        \"feast_credit_scoring_client\": yaml_content\n",
    "    }\n",
    "\n",
    "    print(\"=\" * 50)\n",
    "    print(\"This script will create feature store YAMLs and objects from client configs.\")\n",
    "    print(f\"Processing {len(client_configs)} selected configurations...\")\n",
    "    \n",
    "    # Process the configs\n",
    "    results = process_client_configs(client_configs)\n",
    "    \n",
    "    # Print summary\n",
    "    print_summary(results)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    main()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "project_name = \"credit_scoring_local\"\n",
    "project = fs_credit_scoring_local.get_project(project_name)\n",
    "\n",
    "# 1. Assert object returned\n",
    "assert project is not None, f\"❌ get_project('{project_name}') returned None\"\n",
    "\n",
    "# 2. Extract project name (works for dict or Feast object)\n",
    "if isinstance(project, dict):\n",
    "    returned_name = project.get(\"spec\", {}).get(\"name\")\n",
    "else:\n",
    "    # Feast Project object\n",
    "    returned_name = getattr(project, \"name\", None)\n",
    "    if not returned_name and hasattr(project, \"spec\") and hasattr(project.spec, \"name\"):\n",
    "        returned_name = project.spec.name\n",
    "\n",
    "# 3. Assert that name exists\n",
    "assert returned_name, f\"❌ Returned project does not contain a valid name: {project}\"\n",
    "\n",
    "print(\"• Project Name Returned:\", returned_name)\n",
    "\n",
    "# 4. Assert the name matches expected\n",
    "assert returned_name == project_name, (\n",
    "    f\"❌ Expected project '{project_name}', but got '{returned_name}'\"\n",
    ")\n",
    "\n",
    "print(f\"\\n✓ get_project('{project_name}') validation passed!\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feast_list_functions = [\n",
    "    \"list_projects\",\n",
    "    \"list_entities\",\n",
    "    \"list_feature_views\",\n",
    "    \"list_all_feature_views\",\n",
    "    \"list_batch_feature_views\",\n",
    "    \"list_on_demand_feature_views\",\n",
    "]\n",
    "\n",
    "# validates feast list methods returns data and method type\n",
    "def validate_list_method(fs_obj, method_name):\n",
    "    assert hasattr(fs_obj, method_name), f\"Method not found: {method_name}\"\n",
    "\n",
    "    method = getattr(fs_obj, method_name)\n",
    "    result = method()\n",
    "\n",
    "    assert isinstance(result, list), (\n",
    "        f\"{method_name}() must return a list, got {type(result)}\"\n",
    "    )\n",
    "    assert len(result) > 0, (\n",
    "        f\"{method_name}() returned an empty list — expected data\"\n",
    "    )\n",
    "\n",
    "    print(f\"✓ {method_name}() returned {len(result)} items\")\n",
    "\n",
    "for m in feast_list_functions:\n",
    "    validate_list_method(fs_credit_scoring_local, m)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feast_list_functions = [\n",
    "    \"list_feature_services\",\n",
    "    # \"list_permissions\",\n",
    "    \"list_saved_datasets\",\n",
    "]\n",
    "\n",
    "# validates feast methods exists and type is valid\n",
    "def validate_list_func(fs_obj, method_name):\n",
    "    assert hasattr(fs_obj, method_name), f\"Method not found: {method_name}\"\n",
    "\n",
    "    method = getattr(fs_obj, method_name)\n",
    "\n",
    "    result = method()\n",
    "\n",
    "    assert isinstance(result, list), (\n",
    "        f\"{method_name}() must return a list, got {type(result)}\"\n",
    "    )\n",
    "\n",
    "for m in feast_list_functions:\n",
    "    validate_list_func(fs_credit_scoring_local, m)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# validate_list_data_sources for with and without permissions \n",
    "\n",
    "import os\n",
    "from feast.errors import FeastPermissionError\n",
    "\n",
    "def validate_list_data_sources(fs_obj):\n",
    "    \"\"\"\n",
    "    Validates list_data_sources() with special handling for Kubernetes auth mode.\n",
    "    If CONFIGMAP_DATA indicates auth=kubernetes, expect FeastPermissionError.\n",
    "    Otherwise validate output type normally.\n",
    "    \"\"\"\n",
    "    auth_mode = os.getenv(\"CONFIGMAP_DATA\")\n",
    "\n",
    "    # Case 1: Kubernetes auth → expect permission error\n",
    "    if \"kubernetes\" in auth_mode.lower():\n",
    "        try:\n",
    "            fs_obj.list_data_sources()\n",
    "            raise AssertionError(\n",
    "                \"Expected FeastPermissionError due to Kubernetes auth, but the call succeeded.\"\n",
    "            )\n",
    "        except FeastPermissionError as e:\n",
    "            # Correct, this is expected\n",
    "            return\n",
    "        except Exception as e:\n",
    "            raise AssertionError(\n",
    "                f\"Expected FeastPermissionError, but got different exception: {type(e)} - {e}\"\n",
    "            )\n",
    "\n",
    "    # Case 2: Non-Kubernetes auth → normal path\n",
    "    assert hasattr(fs_obj, \"list_data_sources\"), \"Method not found: list_data_sources\"\n",
    "    result = fs_obj.list_data_sources()\n",
    "    assert isinstance(result, list), (\n",
    "        f\"list_data_sources() must return a list, got {type(result)}\"\n",
    "    )\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "entity = fs_credit_scoring_local.get_entity(\"dob_ssn\")\n",
    "\n",
    "assert entity is not None, \"❌ Entity 'dob_ssn' not found!\"\n",
    "assert entity.name == \"dob_ssn\", f\"❌ Entity name mismatch: {entity.name}\"\n",
    "\n",
    "print(\"✓ Entity validation successful!\\n\", entity.name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "fv = fs_credit_scoring_local.get_feature_view(\"credit_history\")\n",
    "\n",
    "assert fv is not None, \"❌ FeatureView 'credit_history' not found!\"\n",
    "assert fv.name == \"credit_history\", f\"❌ Name mismatch: {fv.name}\"\n",
    "\n",
    "print(\"• FeatureView : validation successful!\", fv.name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from feast.errors import FeastPermissionError\n",
    "\n",
    "def validate_get_data_source(fs_obj, name: str):\n",
    "    auth_mode = os.getenv(\"CONFIGMAP_DATA\", \"\")\n",
    "\n",
    "    print(\"📌 CONFIGMAP_DATA:\", auth_mode)\n",
    "\n",
    "    # If Kubernetes auth is enabled → expect permission error\n",
    "    if \"auth\" in \"kubernetes\" in auth_mode.lower():\n",
    "        print(f\"🔒 Kubernetes auth detected, expecting permission error for get_data_source('{name}')\")\n",
    "\n",
    "        try:\n",
    "            fs_obj.get_data_source(name)\n",
    "            raise AssertionError(\n",
    "                f\"❌ Expected FeastPermissionError when accessing data source '{name}', but call succeeded\"\n",
    "            )\n",
    "\n",
    "        except FeastPermissionError as e:\n",
    "            print(f\"✅ Correctly blocked with FeastPermissionError: {e}\")\n",
    "            return\n",
    "\n",
    "        except Exception as e:\n",
    "            raise AssertionError(\n",
    "                f\"❌ Expected FeastPermissionError but got {type(e)}: {e}\"\n",
    "            )\n",
    "\n",
    "    # Otherwise → normal validation\n",
    "    print(f\"🔍 Fetching data source '{name}'...\")\n",
    "\n",
    "    ds = fs_obj.get_data_source(name)\n",
    "\n",
    "    print(\"\\n📌 Data Source Object:\")\n",
    "    print(ds)\n",
    "\n",
    "    assert ds.name == name, (\n",
    "        f\"❌ Expected name '{name}', got '{ds.name}'\"\n",
    "    )\n",
    "\n",
    "    print(f\"✅ Data source '{name}' exists and is correctly configured.\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feast_features = [\n",
    "    \"zipcode_features:city\",\n",
    "    \"zipcode_features:state\",\n",
    "]\n",
    "\n",
    "entity_rows = [{\n",
    "    \"zipcode\": 1463,\n",
    "    \"dob_ssn\": \"19530219_5179\"\n",
    "}]\n",
    "\n",
    "response = fs_credit_scoring_local.get_online_features(\n",
    "    features=feast_features,\n",
    "    entity_rows=entity_rows,\n",
    ").to_dict()\n",
    "\n",
    "print(\"Actual response:\", response)\n",
    "\n",
    "expected = {\n",
    "    'zipcode': [1463],\n",
    "    'dob_ssn': ['19530219_5179'],\n",
    "    'city': ['PEPPERELL'],\n",
    "    'state': ['MA'],\n",
    "}\n",
    "\n",
    "assert response == expected"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "# Input entity dataframe\n",
    "entity_df = pd.DataFrame({\n",
    "    \"dob_ssn\": [\"19530219_5179\"],\n",
    "    \"zipcode\": [1463],\n",
    "    \"event_timestamp\": [pd.Timestamp(\"2020-04-26 18:01:04\")]\n",
    "})\n",
    "\n",
    "feast_features = [\n",
    "    \"zipcode_features:city\",\n",
    "    \"zipcode_features:state\",\n",
    "    \"credit_history:credit_card_due\",\n",
    "    \"credit_history:mortgage_due\",\n",
    "]\n",
    "\n",
    "# Retrieve historical features\n",
    "historical_df = fs_credit_scoring_local.get_historical_features(\n",
    "    entity_df=entity_df,\n",
    "    features=feast_features,\n",
    ").to_df()\n",
    "\n",
    "print(\"Historical DF:\\n\", historical_df)\n",
    "\n",
    "# Validate dataframe is not empty\n",
    "assert not historical_df.empty, \" Historical features dataframe is empty!\"\n",
    "\n",
    "# 2. Validate required columns exist\n",
    "expected_cols = {\n",
    "    \"dob_ssn\", \"zipcode\", \"event_timestamp\",\n",
    "    \"city\", \"state\",\n",
    "    \"credit_card_due\", \"mortgage_due\"\n",
    "}\n",
    "\n",
    "missing_cols = expected_cols - set(historical_df.columns)\n",
    "assert not missing_cols, f\" Missing columns in result: {missing_cols}\"\n",
    "\n",
    "# 3. Validate city/state are non-null (critical features)\n",
    "assert pd.notna(historical_df.loc[0, \"city\"]), \" 'city' value is null!\"\n",
    "assert pd.notna(historical_df.loc[0, \"state\"]), \" 'state' value is null!\"\n",
    "\n",
    "# 4. Validate entity matches input\n",
    "assert historical_df.loc[0, \"zipcode\"] == 1463, \" zipcode mismatch!\"\n",
    "assert historical_df.loc[0, \"dob_ssn\"] == \"19530219_5179\", \"❌ dob_ssn mismatch!\"\n",
    "\n",
    "print(\"✅ All validations passed successfully!\")\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
